﻿using System;
using System.Reflection;
using log4net;
using log4net.Core;
using log4net.Repository;
using log4net.Repository.Hierarchy;
using roundhouse.consoles;
using roundhouse.databases;
using roundhouse.folders;
using roundhouse.infrastructure;
using roundhouse.infrastructure.app;
using roundhouse.infrastructure.app.logging;
using roundhouse.infrastructure.commandline.options;
using roundhouse.infrastructure.containers;
using roundhouse.infrastructure.extensions;
using roundhouse.infrastructure.filesystem;
using roundhouse.migrators;
using roundhouse.resolvers;
using roundhouse.runners;

namespace roundhouse.console
{
    public class Program
    {
        private static readonly ILog the_logger = LogManager.GetLogger(typeof(Program));

        private static void Main(string[] args)
        {
            Log4NetAppender.configure();

            int exit_code = 0;

            try
            {
                // determine if this a call to the diff or the migrator
                if (string.Join("|", args).to_lower().Contains("version") && args.Length == 1)
                {
                    report_version();
                }
                else if (string.Join("|", args).to_lower().Contains("rh.redgate.diff"))
                {
                    run_diff_utility(set_up_configuration_and_build_the_container(args));
                }
                else
                {
                    run_migrator(set_up_configuration_and_build_the_container(args));
                }
            }
            catch (Exception ex)
            {
                the_logger.Error(ex.Message, ex);
                exit_code = 1;
            }
            finally
            {
#if DEBUG
                System.Console.WriteLine("Press any key to continue...");
                System.Console.ReadKey();
#endif
                Environment.Exit(exit_code);
            }
        }

        public static void report_version()
        {
            string version = VersionInformation.get_current_assembly_version();
            the_logger.InfoFormat("{0} - version {1} from http://projectroundhouse.org.", ApplicationParameters.name, version);
        }

        public static ConfigurationPropertyHolder set_up_configuration_and_build_the_container(string[] args)
        {
            ConfigurationPropertyHolder configuration = new DefaultConfiguration();
            parse_arguments_and_set_up_configuration(configuration, args);

            ApplicationConfiguraton.set_defaults_if_properties_are_not_set(configuration);
            ApplicationConfiguraton.build_the_container(configuration);

            return configuration;
        }

        private static void parse_arguments_and_set_up_configuration(ConfigurationPropertyHolder configuration, string[] args)
        {
            bool help = false;

            OptionSet option_set = new OptionSet()
                .Add("?|help|h",
                     "Prints out the options.",
                     option => help = option != null)
                .Add("d=|db=|database=|databasename=",
                     "REQUIRED: DatabaseName - The database you want to create/migrate.",
                     option => configuration.DatabaseName = option)
                .Add("c=|cs=|connstring=|connectionstring=",
                     string.Format(
                         "REQUIRED: ConnectionString - As an alternative to ServerName and Database - You can provide an entire connection string instead."),
                     option => configuration.ConnectionString = option)
                .Add("f=|files=|sqlfilesdirectory=",
                     string.Format("SqlFilesDirectory - The directory where your SQL scripts are. Defaults to \"{0}\".",
                                   ApplicationParameters.default_files_directory),
                     option => configuration.SqlFilesDirectory = option)
                .Add("s=|server=|servername=|instance=|instancename=",
                     string.Format(
                         "ServerName - The server and instance you would like to run on. (local) and (local)\\SQL2008 are both valid values. Defaults to \"{0}\".",
                         ApplicationParameters.default_server_name),
                     option => configuration.ServerName = option)
                .Add("csa=|connstringadmin=|connectionstringadministration=",
                     string.Format(
                         "ConnectionStringAdministration - This is used for connecting to master when you may have a different uid and password than normal."),
                     option => configuration.ConnectionStringAdmin = option)
                .Add("ct=|commandtimeout=",
                     string.Format(
                         "CommandTimeout - This is the timeout when commands are run. This is not for admin commands or restore. Defaults to \"{0}\".",
                         ApplicationParameters.default_command_timeout),
                     option => configuration.CommandTimeout = int.Parse(option))
                .Add("cta=|commandtimeoutadmin=",
                     string.Format(
                         "CommandTimeoutAdministration - This is the timeout when administration commands are run (except for restore, which has its own). Defaults to \"{0}\".",
                         ApplicationParameters.default_admin_command_timeout),
                     option => configuration.CommandTimeoutAdmin = int.Parse(option))
                //database type
                .Add("dt=|dbt=|databasetype=",
                     string.Format(
                         "DatabaseType - Tells RH what type of database it is running on. This is a plugin model. This is the fully qualified name of a class that implements the interface roundhouse.sql.Database, roundhouse. If you have your own assembly, just set it next to rh.exe and set this value appropriately. Defaults to 'sqlserver' which is a synonym for '{0}'.",
                         ApplicationParameters.default_database_type),
                     option => configuration.DatabaseType = option)
                // versioning
                .Add("r=|repo=|repositorypath=",
                     string.Format(
                         "RepositoryPath - The repository. A string that can be anything. Used to track versioning along with the version. Defaults to null."),
                     option => configuration.RepositoryPath = option)
                .Add("vf=|versionfile=",
                     string.Format("VersionFile - Either a .XML file, a .DLL or a .TXT file that a version can be resolved from. Defaults to \"{0}\".",
                                   ApplicationParameters.default_version_file),
                     option => configuration.VersionFile = option)
                .Add("vx=|versionxpath=",
                     string.Format("VersionXPath - Works in conjunction with an XML version file. Defaults to \"{0}\".",
                                   ApplicationParameters.default_version_x_path),
                     option => configuration.VersionXPath = option)
                // folders
                .Add("ad=|alterdatabase=|alterdatabasefolder=|alterdatabasefoldername=",
                     string.Format(
                         "AlterDatabaseFolderName - The name of the folder where you keep your alter database scripts. Read up on token replacement. You will want to use {{DatabaseName}} here instead of specifying a database name. Will recurse through subfolders. Defaults to \"{0}\".",
                         ApplicationParameters.default_alter_database_folder_name),
                     option => configuration.AlterDatabaseFolderName = option)
                .Add("racd=|runaftercreatedatabase=|runaftercreatedatabasefolder=|runaftercreatedatabasefoldername=",
                     string.Format(
                         "RunAfterCreateDatabaseFolderName - The name of the folder where you will keep scripts that ONLY run after a database is created.  Will recurse through subfolders. Defaults to \"{0}\".",
                         ApplicationParameters.default_run_after_create_database_folder_name),
                     option => configuration.RunAfterCreateDatabaseFolderName = option)
                .Add("rb=|runbefore=|runbeforeupfolder=|runbeforeupfoldername=",
                     string.Format(
                         "RunBeforeUpFolderName - The name of the folder where you keep scripts that you want to run before your update scripts. Will recurse through subfolders. Defaults to \"{0}\".",
                         ApplicationParameters.default_run_before_up_folder_name),
                     option => configuration.RunBeforeUpFolderName = option)
                .Add("u=|up=|upfolder=|upfoldername=",
                     string.Format(
                         "UpFolderName - The name of the folder where you keep your update scripts. Will recurse through subfolders. Defaults to \"{0}\".",
                         ApplicationParameters.default_up_folder_name),
                     option => configuration.UpFolderName = option)
                .Add("do=|down=|downfolder=|downfoldername=",
                     string.Format(
                         "DownFolderName - The name of the folder where you keep your versioning down scripts. Will recurse through subfolders. Defaults to \"{0}\".",
                         ApplicationParameters.default_down_folder_name),
                     option => configuration.DownFolderName = option)
                .Add("rf=|runfirst=|runfirstfolder=|runfirstafterupdatefolder=|runfirstafterupdatefoldername=",
                     string.Format(
                         "RunFirstAfterUpdateFolderName - The name of the folder where you keep any functions, views, or sprocs that are order dependent. If you have a function that depends on a view, you definitely need the view in this folder. Will recurse through subfolders. Defaults to \"{0}\".",
                         ApplicationParameters.default_run_first_after_up_folder_name),
                     option => configuration.RunFirstAfterUpFolderName = option)
                .Add("fu=|functions=|functionsfolder=|functionsfoldername=",
                     string.Format(
                         "FunctionsFolderName - The name of the folder where you keep your functions. Will recurse through subfolders. Defaults to \"{0}\".",
                         ApplicationParameters.default_functions_folder_name),
                     option => configuration.FunctionsFolderName = option)
                .Add("vw=|views=|viewsfolder=|viewsfoldername=",
                     string.Format("ViewsFolderName - The name of the folder where you keep your views. Will recurse through subfolders. Defaults to \"{0}\".",
                                   ApplicationParameters.default_views_folder_name),
                     option => configuration.ViewsFolderName = option)
                .Add("sp=|sprocs=|sprocsfolder=|sprocsfoldername=",
                     string.Format(
                         "SprocsFolderName - The name of the folder where you keep your stored procedures. Will recurse through subfolders. Defaults to \"{0}\".",
                         ApplicationParameters.default_sprocs_folder_name),
                     option => configuration.SprocsFolderName = option)
                .Add("ix=|indexes=|indexesfolder=|indexesfoldername=",
                     string.Format(
                         "IndexesFolderName - The name of the folder where you keep your indexes. Will recurse through subfolders. Defaults to \"{0}\".",
                         ApplicationParameters.default_indexes_folder_name),
                     option => configuration.IndexesFolderName = option)
                .Add("ra=|runAfterOtherAnyTimeScripts=|runAfterOtherAnyTimeScriptsfolder=|runAfterOtherAnyTimeScriptsfoldername=",
                     string.Format(
                         "RunAfterOtherAnyTimeScriptsFolderName - The name of the folder where you keep scripts that will be run after all of the other any time scripts complete. Will recurse through subfolders. Defaults to \"{0}\".",
                         ApplicationParameters.default_runAfterOtherAnyTime_folder_name),
                     option => configuration.RunAfterOtherAnyTimeScriptsFolderName = option)
                .Add("p=|permissions=|permissionsfolder=|permissionsfoldername=",
                     string.Format(
                         "PermissionsFolderName - The name of the folder where you keep your permissions scripts. Will recurse through subfolders. Defaults to \"{0}\".",
                         ApplicationParameters.default_permissions_folder_name),
                     option => configuration.PermissionsFolderName = option)
                // roundhouse items
                .Add("sc=|schema=|schemaname=",
                     string.Format(
                         "SchemaName - This is the schema where RH stores it's tables. Once you set this a certain way, do not change this. This is definitely running with scissors and very sharp. I am allowing you to have flexibility, but because this is a knife you can still get cut if you use it wrong. I'm just saying. You've been warned. Defaults to \"{0}\".",
                         ApplicationParameters.default_roundhouse_schema_name),
                     option => configuration.SchemaName = option)
                .Add("vt=|versiontable=|versiontablename=",
                     string.Format(
                         "VersionTableName - This is the table where RH stores versioning information. Once you set this, do not change this. This is definitely running with scissors and very sharp. Defaults to \"{0}\".",
                         ApplicationParameters.default_version_table_name),
                     option => configuration.VersionTableName = option)
                .Add("srt=|scriptsruntable=|scriptsruntablename=",
                     string.Format(
                         "ScriptsRunTableName - This is the table where RH stores information about scripts that have been run. Once you set this a certain way, do not change this. This is definitely running with scissors and very sharp. Defaults to \"{0}\".",
                         ApplicationParameters.default_scripts_run_table_name),
                     option => configuration.ScriptsRunTableName = option)
                .Add("sret=|scriptsrunerrorstable=|scriptsrunerrorstablename=",
                     string.Format(
                         "ScriptsRunErrorsTableName - This is the table where RH stores information about scripts that have been run with errors. Once you set this a certain way, do not change this. This is definitelly running with scissors and very sharp. Defaults to \"{0}\".",
                         ApplicationParameters.default_scripts_run_errors_table_name),
                     option => configuration.ScriptsRunErrorsTableName = option)
                //environment
                .Add("env=|environment=|environmentname=",
                     string.Format(
                         "EnvironmentName - This allows RH to be environment aware and only run scripts that are in a particular environment based on the naming of the script. LOCAL.something.ENV.sql would only be run in the LOCAL environment. Defaults to \"{0}\".",
                         ApplicationParameters.default_environment_name),
                     option => configuration.EnvironmentName = option)
                //restore
                .Add("restore",
                     "Restore - This instructs RH to do a restore (with the restorefrompath parameter) of a database before running migration scripts. Defaults to false.",
                     option => configuration.Restore = option != null)
                .Add("rfp=|restorefrom=|restorefrompath=",
                     "RestoreFromPath - This tells the restore where to get to the backed up database. Defaults to null. Required if /restore has been set. NOTE: will try to use Litespeed for the restore if the last two characters of the name are LS (as in DudeLS.bak).",
                     option => configuration.RestoreFromPath = option)
                .Add("rco=|restoreoptions=|restorecustomoptions=",
                     "RestoreCustomOptions - This provides the restore any custom options as in MOVE='Somewhere or another'.",
                     option => configuration.RestoreCustomOptions = option)
                .Add("rt=|restoretimeout=",
                     "RestoreTimeout - Allows you to specify a restore timeout in seconds. The default is 900 seconds.",
                     option => configuration.RestoreTimeout = int.Parse(option))
                //custom create database
                .Add("cds=|createdatabasescript=|createdatabasecustomscript=",
                     "CreateDatabaseCustomScript - This instructs RH to use this script for creating a database instead of the default based on the SQLType.",
                     option => configuration.CreateDatabaseCustomScript = option)
                //drop
                .Add("drop",
                     "Drop - This instructs RH to remove a database and not run migration scripts. Defaults to false.",
                     option => configuration.Drop = option != null)
                //don't create the database if it doesn't exist
                .Add("dc|dnc|donotcreatedatabase",
                     "DoNotCreateDatabase - This instructs RH to not create a database if it does not exists. Defaults to false.",
                     option => configuration.DoNotCreateDatabase = option != null)
                //output
                .Add("o=|output=|outputpath=",
                     string.Format(
                         "OutputPath - This is where everything related to the migration is stored. This includes any backups, all items that ran, permission dumps, logs, etc. Defaults to \"{0}\".",
                         ApplicationParameters.default_output_path),
                     option => configuration.OutputPath = option)
                //output
                .Add("disableoutput",
                     string.Format(
                         "DisableoOutput - Disable output of backups, items ran, permissions dumps, etc. Log files are kept. Useful for example in CI environment. Defaults to \"{0}\".",
                         ApplicationParameters.default_disable_output),
                     option => configuration.DisableOutput = option != null)
                //warn on changes
                .Add("w|warnononetimescriptchanges",
                     "WarnOnOneTimeScriptChanges - Instructs RH to execute changed one time scripts (DDL/DML in Up folder) that have previously been run against the database instead of failing. A warning is logged for each one time scripts that is rerun. Defaults to false.",
                     option => configuration.WarnOnOneTimeScriptChanges = option != null)
                //silent?
                .Add("silent|ni|noninteractive",
                     "Silent - tells RH not to ask for any input when it runs. Defaults to false.",
                     option => configuration.Silent = option != null)
                //transaction
                .Add("t|trx|transaction|wt|withtransaction",
                     "WithTransaction - This instructs RH to run inside of a transaction. Defaults to false.",
                     option => configuration.WithTransaction = option != null)
                //recovery mode
                .Add("simple",
                     "RecoveryModeSimple - This instructs RH to set the database recovery mode to simple recovery. Defaults to false.",
                     option => configuration.RecoveryModeSimple = option != null)
                .Add("rcm=|recoverymode=",
                     "RecoveryMode - This instructs RH to set the database recovery mode to Simple|Full|NoChange. Defaults to NoChange.",
                     option => configuration.RecoveryMode = (RecoveryMode)Enum.Parse(typeof(RecoveryMode), option, true))
                //debug
                .Add("debug",
                     "Debug - This instructs RH to write out all messages. Defaults to false.",
                     option => configuration.Debug = option != null)
                //force all anytime scripts
                .Add("runallanytimescripts|forceanytimescripts",
                     "RunAllAnyTimeScripts - This instructs RH to run any time scripts every time it is run. Defaults to false.",
                     option => configuration.RunAllAnyTimeScripts = option != null)
                //disable token replacement
                .Add("disabletokens|disabletokenreplacement",
                     "DisableTokenReplacement - This instructs RH to not perform token replacement {{somename}}. Defaults to false.",
                     option => configuration.DisableTokenReplacement = option != null)
                //recorders
                .Add("baseline",
                     "Baseline - This instructs RH to create an insert for its recording tables, but not to actually run anything against the database. Use this option if you already have scripts that have been run through other means (and BEFORE you start the new ones).",
                     option => configuration.Baseline = option != null)
                .Add("dryrun",
                     "DryRun - This instructs RH to log what would have run, but not to actually run anything against the database. Use this option if you are trying to figure out what RH is going to do.",
                     option => configuration.DryRun = option != null)
                .Add("searchallinsteadoftraverse=|searchallsubdirectoriesinsteadoftraverse=",
                     "SearchAllSubdirectoriesInsteadOfTraverse - Each Migration folder's subdirectories are traversed by default. This option pulls back scripts from the main directory and all subdirectories at once. Defaults to 'false'",
                     option => configuration.SearchAllSubdirectoriesInsteadOfTraverse = option != null)
                ;

            try
            {
                option_set.Parse(args);
            }
            catch (OptionException)
            {
                show_help("Error, usage is:", option_set);
            }

            if (help)
            {
                the_logger.Info("Usage of RoundhousE (RH)");
                string usage_message =
                    string.Format(
                        "rh.exe /d[atabase] VALUE OR rh.exe /c[onnection]s[tring] VALUE followed by all the optional parameters {0}" +
                        "[" +
                        "/[sql]f[ilesdirectory] VALUE " +
                        "/s[ervername] VALUE " +
                        "/c[onnection]s[tring]a[dministration] VALUE " +
                        "/c[ommand]t[imeout] VALUE /c[ommand]t[imeout]a[dmin] VALUE " +
                        "/r[epositorypath] VALUE /v[ersion]f[ile] VALUE /v[ersion]x[path] VALUE " +
                        "/a[lter]d[atabasefoldername] /r[un]a[fter]c[reate]d[atabasefoldername] VALUE VALUE " +
                        "/r[un]b[eforeupfoldername] VALUE /u[pfoldername] VALUE /do[wnfoldername] VALUE " +
                        "/r[un]f[irstafterupdatefoldername] VALUE /fu[nctionsfoldername] VALUE /v[ie]w[sfoldername] VALUE " +
                        "/sp[rocsfoldername] VALUE /i[nde]x[foldername] VALUE /p[ermissionsfoldername] VALUE " +
                        "/sc[hemaname] VALUE /v[ersion]t[ablename] VALUE /s[cripts]r[un]t[ablename] VALUE /s[cripts]r[un]e[rrors]t[ablename] VALUE " +
                        "/env[ironmentname] VALUE " +
                        "/restore /r[estore]f[rom]p[ath] VALUE /r[estore]c[ustom]o[ptions] VALUE /r[estore]t[imeout] VALUE" +
                        "/c[reate]d[atabasecustom]s[cript] VALUE " +
                        "/env[ironmentname] VALUE " +
                        "/o[utputpath] VALUE " +
                        "/w[arnononetimescriptchanges] " +
                        "/silent " +
                        "/pde[PerDirectoryExecution] " +
                        "/d[atabase]t[ype] VALUE " +
                        "/drop " +
                        "/d[onot]c[reatedatabase] " +
                        "/t[ransaction] " +
                        "/r[e]c[overy]m[ode] NoChange|Simple|Full" +
                        "/debug " +
                        "/runallanytimescripts " +
                        "/disabletokenreplacement " +
                        "/baseline " +
                        "/dryrun " +
                        "/search[allsubdirectories]insteadoftraverse" +
                        "]", Environment.NewLine);
                show_help(usage_message, option_set);
            }

            if (string.IsNullOrEmpty(configuration.DatabaseName) && string.IsNullOrEmpty(configuration.ConnectionString))
            {
                show_help("Error: You must specify Database Name (/d) OR Connection String (/cs) at a minimum to use RoundhousE.", option_set);
            }

            if (configuration.Restore && string.IsNullOrEmpty(configuration.RestoreFromPath))
            {
                show_help(
                    "If you set Restore to true, you must specify a location for the database to be restored from (RestoreFromPath /restorefrompath).",
                    option_set);
            }
        }

        public static void show_help(string message, OptionSet option_set)
        {
            //Console.Error.WriteLine(message);
            the_logger.Info(message);
            option_set.WriteOptionDescriptions(Console.Error);
            Environment.Exit(-1);
        }

        public static void run_migrator(ConfigurationPropertyHolder configuration)
        {
            RoundhouseMigrationRunner migration_runner = get_migration_runner(configuration);
            migration_runner.run();

            if (!configuration.Silent)
            {
                Console.WriteLine("{0}Please press enter to continue...", Environment.NewLine);
                Console.Read();
            }
        }

        private static void run_diff_utility(ConfigurationPropertyHolder configuration)
        {
            bool silent = configuration.Silent;

            RoundhouseRedGateCompareRunner diff_runner = get_diff_runner(configuration, get_migration_runner(configuration));
            diff_runner.run();

            if (!silent)
            {
                Console.WriteLine("{0}Please press enter to continue...", Environment.NewLine);
                Console.Read();
            }
        }

        private static RoundhouseMigrationRunner get_migration_runner(ConfigurationPropertyHolder configuration)
        {
            return new RoundhouseMigrationRunner(
                configuration.RepositoryPath,
                Container.get_an_instance_of<environments.Environment>(),
                Container.get_an_instance_of<KnownFolders>(),
                Container.get_an_instance_of<FileSystemAccess>(),
                Container.get_an_instance_of<DatabaseMigrator>(),
                Container.get_an_instance_of<VersionResolver>(),
                configuration.Silent,
                configuration.Drop,
                configuration.DoNotCreateDatabase,
                configuration.WithTransaction,
                configuration.RecoveryModeSimple,
                configuration);
        }

        private static RoundhouseRedGateCompareRunner get_diff_runner(ConfigurationPropertyHolder configuration, RoundhouseMigrationRunner migration_runner)
        {
            return new RoundhouseRedGateCompareRunner(
                Container.get_an_instance_of<KnownFolders>(),
                Container.get_an_instance_of<FileSystemAccess>(),
                configuration, migration_runner);
        }
    }
}
